/*
 * Copyright (c) 2022, Alliance for Open Media. All rights reserved
 *
 * This source code is subject to the terms of the BSD 2 Clause License and
 * the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
 * was not distributed with this source code in the LICENSE file, you can
 * obtain it at www.aomedia.org/license/software. If the Alliance for Open
 * Media Patent License 1.0 was not distributed with this source code in the
 * PATENTS file, you can obtain it at www.aomedia.org/license/patent.
 */
#include "av1/qmode_rc/ratectrl_qmode.h"

#include <algorithm>
#include <cassert>
#include <climits>
#include <functional>
#include <numeric>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
#include <vector>

#include "aom/aom_codec.h"
#include "av1/encoder/pass2_strategy.h"
#include "av1/encoder/ratectrl.h"
#include "av1/encoder/tpl_model.h"

namespace aom {

// This is used before division to ensure that the divisor isn't zero or
// too close to zero.
static double ModifyDivisor(double divisor) {
  const double kEpsilon = 0.0000001;
  return (divisor < 0 ? std::min(divisor, -kEpsilon)
                      : std::max(divisor, kEpsilon));
}

GopFrame GopFrameInvalid() {
  GopFrame gop_frame = {};
  gop_frame.is_valid = false;
  gop_frame.coding_idx = -1;
  gop_frame.order_idx = -1;
  return gop_frame;
}

void SetGopFrameByType(GopFrameType gop_frame_type, GopFrame *gop_frame) {
  gop_frame->update_type = gop_frame_type;
  switch (gop_frame_type) {
    case GopFrameType::kRegularKey:
      gop_frame->is_key_frame = 1;
      gop_frame->is_arf_frame = 0;
      gop_frame->is_show_frame = 1;
      gop_frame->is_golden_frame = 1;
      gop_frame->encode_ref_mode = EncodeRefMode::kRegular;
      break;
    case GopFrameType::kRegularGolden:
      gop_frame->is_key_frame = 0;
      gop_frame->is_arf_frame = 0;
      gop_frame->is_show_frame = 1;
      gop_frame->is_golden_frame = 1;
      gop_frame->encode_ref_mode = EncodeRefMode::kRegular;
      break;
    case GopFrameType::kRegularArf:
      gop_frame->is_key_frame = 0;
      gop_frame->is_arf_frame = 1;
      gop_frame->is_show_frame = 0;
      gop_frame->is_golden_frame = 1;
      gop_frame->encode_ref_mode = EncodeRefMode::kRegular;
      break;
    case GopFrameType::kIntermediateArf:
      gop_frame->is_key_frame = 0;
      gop_frame->is_arf_frame = 1;
      gop_frame->is_show_frame = 0;
      gop_frame->is_golden_frame = gop_frame->layer_depth <= 2 ? 1 : 0;
      gop_frame->encode_ref_mode = EncodeRefMode::kRegular;
      break;
    case GopFrameType::kRegularLeaf:
      gop_frame->is_key_frame = 0;
      gop_frame->is_arf_frame = 0;
      gop_frame->is_show_frame = 1;
      gop_frame->is_golden_frame = 0;
      gop_frame->encode_ref_mode = EncodeRefMode::kRegular;
      break;
    case GopFrameType::kIntermediateOverlay:
      gop_frame->is_key_frame = 0;
      gop_frame->is_arf_frame = 0;
      gop_frame->is_show_frame = 1;
      gop_frame->is_golden_frame = 0;
      gop_frame->encode_ref_mode = EncodeRefMode::kShowExisting;
      break;
    case GopFrameType::kOverlay:
      gop_frame->is_key_frame = 0;
      gop_frame->is_arf_frame = 0;
      gop_frame->is_show_frame = 1;
      gop_frame->is_golden_frame = 0;
      gop_frame->encode_ref_mode = EncodeRefMode::kOverlay;
      break;
  }
}

GopFrame GopFrameBasic(int global_coding_idx_offset,
                       int global_order_idx_offset, int coding_idx,
                       int order_idx, int depth, int display_idx,
                       GopFrameType gop_frame_type) {
  GopFrame gop_frame = {};
  gop_frame.is_valid = true;
  gop_frame.coding_idx = coding_idx;
  gop_frame.order_idx = order_idx;
  gop_frame.display_idx = display_idx;
  gop_frame.global_coding_idx = global_coding_idx_offset + coding_idx;
  gop_frame.global_order_idx = global_order_idx_offset + order_idx;
  gop_frame.layer_depth = depth + kLayerDepthOffset;
  gop_frame.colocated_ref_idx = -1;
  gop_frame.update_ref_idx = -1;
  SetGopFrameByType(gop_frame_type, &gop_frame);
  return gop_frame;
}

// This function create gop frames with indices of display order from
// order_start to order_end - 1. The function will recursively introduce
// intermediate ARF untill maximum depth is met or the number of regular frames
// in between two ARFs are less than 3. Than the regular frames will be added
// into the gop_struct.
void ConstructGopMultiLayer(GopStruct *gop_struct,
                            RefFrameManager *ref_frame_manager, int max_depth,
                            int depth, int order_start, int order_end) {
  GopFrame gop_frame;
  int num_frames = order_end - order_start;
  const int global_coding_idx_offset = gop_struct->global_coding_idx_offset;
  const int global_order_idx_offset = gop_struct->global_order_idx_offset;
  // If there are less than kMinIntervalToAddArf frames, stop introducing ARF
  if (depth < max_depth && num_frames >= kMinIntervalToAddArf) {
    int order_mid = (order_start + order_end) / 2;
    // intermediate ARF
    gop_frame = GopFrameBasic(
        global_coding_idx_offset, global_order_idx_offset,
        static_cast<int>(gop_struct->gop_frame_list.size()), order_mid, depth,
        gop_struct->display_tracker, GopFrameType::kIntermediateArf);
    ref_frame_manager->UpdateRefFrameTable(&gop_frame);
    gop_struct->gop_frame_list.push_back(gop_frame);
    ConstructGopMultiLayer(gop_struct, ref_frame_manager, max_depth, depth + 1,
                           order_start, order_mid);
    // show existing intermediate ARF
    gop_frame =
        GopFrameBasic(global_coding_idx_offset, global_order_idx_offset,
                      static_cast<int>(gop_struct->gop_frame_list.size()),
                      order_mid, max_depth, gop_struct->display_tracker,
                      GopFrameType::kIntermediateOverlay);
    ref_frame_manager->UpdateRefFrameTable(&gop_frame);
    gop_struct->gop_frame_list.push_back(gop_frame);
    ++gop_struct->display_tracker;
    ConstructGopMultiLayer(gop_struct, ref_frame_manager, max_depth, depth + 1,
                           order_mid + 1, order_end);
  } else {
    // regular frame
    for (int i = order_start; i < order_end; ++i) {
      gop_frame = GopFrameBasic(
          global_coding_idx_offset, global_order_idx_offset,
          static_cast<int>(gop_struct->gop_frame_list.size()), i, max_depth,
          gop_struct->display_tracker, GopFrameType::kRegularLeaf);
      ref_frame_manager->UpdateRefFrameTable(&gop_frame);
      gop_struct->gop_frame_list.push_back(gop_frame);
      ++gop_struct->display_tracker;
    }
  }
}

GopStruct ConstructGop(RefFrameManager *ref_frame_manager, int show_frame_count,
                       bool has_key_frame, bool has_arf_frame,
                       bool use_prev_arf, int global_coding_idx_offset,
                       int global_order_idx_offset, double base_q_ratio) {
  const int arf_depth = 0;

  GopStruct gop_struct;
  gop_struct.show_frame_count = show_frame_count;
  gop_struct.global_coding_idx_offset = global_coding_idx_offset;
  gop_struct.global_order_idx_offset = global_order_idx_offset;
  gop_struct.base_q_ratio = base_q_ratio;
  int order_start = 0;
  int order_end = show_frame_count - 1;

  gop_struct.display_tracker = 0;

  GopFrame gop_frame;
  if (has_key_frame) {
    const int key_frame_depth = -1;
    ref_frame_manager->Reset();
    gop_frame = GopFrameBasic(
        global_coding_idx_offset, global_order_idx_offset,
        static_cast<int>(gop_struct.gop_frame_list.size()), order_start,
        key_frame_depth, gop_struct.display_tracker, GopFrameType::kRegularKey);
    ref_frame_manager->UpdateRefFrameTable(&gop_frame);
    gop_struct.gop_frame_list.push_back(gop_frame);
    order_start++;
    ++gop_struct.display_tracker;
  } else if (!use_prev_arf) {
    // If we don't use the previous ARF or if it does not exist, add a golden
    // frame first.
    gop_frame = GopFrameBasic(
        global_coding_idx_offset, global_order_idx_offset,
        static_cast<int>(gop_struct.gop_frame_list.size()), order_start,
        arf_depth, gop_struct.display_tracker, GopFrameType::kRegularGolden);
    ref_frame_manager->UpdateRefFrameTable(&gop_frame);
    gop_struct.gop_frame_list.push_back(gop_frame);
    order_start++;
    ++gop_struct.display_tracker;
  }

  if (has_arf_frame) {
    // Use multi-layer pyrmaid coding structure.
    gop_frame = GopFrameBasic(
        global_coding_idx_offset, global_order_idx_offset,
        static_cast<int>(gop_struct.gop_frame_list.size()), order_end,
        arf_depth, gop_struct.display_tracker, GopFrameType::kRegularArf);
    ref_frame_manager->UpdateRefFrameTable(&gop_frame);
    gop_struct.gop_frame_list.push_back(gop_frame);
    ConstructGopMultiLayer(&gop_struct, ref_frame_manager,
                           ref_frame_manager->MaxRefFrame() - 1, arf_depth + 1,
                           order_start, order_end);
    // Overlay
    gop_frame =
        GopFrameBasic(global_coding_idx_offset, global_order_idx_offset,
                      static_cast<int>(gop_struct.gop_frame_list.size()),
                      order_end, ref_frame_manager->MaxRefFrame() - 1,
                      gop_struct.display_tracker, GopFrameType::kOverlay);
    ref_frame_manager->UpdateRefFrameTable(&gop_frame);
    gop_struct.gop_frame_list.push_back(gop_frame);
    ++gop_struct.display_tracker;
  } else {
    // Use IPPP format.
    for (int i = order_start; i <= order_end; ++i) {
      gop_frame = GopFrameBasic(
          global_coding_idx_offset, global_order_idx_offset,
          static_cast<int>(gop_struct.gop_frame_list.size()), i, arf_depth + 1,
          gop_struct.display_tracker, GopFrameType::kRegularLeaf);
      ref_frame_manager->UpdateRefFrameTable(&gop_frame);
      gop_struct.gop_frame_list.push_back(gop_frame);
      ++gop_struct.display_tracker;
    }
  }

  return gop_struct;
}

Status AV1RateControlQMode::SetRcParam(const RateControlParam &rc_param) {
  std::ostringstream error_message;
  if (rc_param.max_gop_show_frame_count <
      std::max(4, rc_param.min_gop_show_frame_count)) {
    error_message << "max_gop_show_frame_count ("
                  << rc_param.max_gop_show_frame_count
                  << ") must be at least 4 and may not be less than "
                     "min_gop_show_frame_count ("
                  << rc_param.min_gop_show_frame_count << ")";
    return { AOM_CODEC_INVALID_PARAM, error_message.str() };
  }
  if (rc_param.ref_frame_table_size < 1 || rc_param.ref_frame_table_size > 8) {
    error_message << "ref_frame_table_size (" << rc_param.ref_frame_table_size
                  << ") must be in the range [1, 8].";
    return { AOM_CODEC_INVALID_PARAM, error_message.str() };
  }
  if (rc_param.max_ref_frames < 1 || rc_param.max_ref_frames > 7) {
    error_message << "max_ref_frames (" << rc_param.max_ref_frames
                  << ") must be in the range [1, 7].";
    return { AOM_CODEC_INVALID_PARAM, error_message.str() };
  }
  if (rc_param.base_q_index < 0 || rc_param.base_q_index > 255) {
    error_message << "base_q_index (" << rc_param.base_q_index
                  << ") must be in the range [0, 255].";
    return { AOM_CODEC_INVALID_PARAM, error_message.str() };
  }
  if (rc_param.frame_width < 16 || rc_param.frame_width > 16384 ||
      rc_param.frame_height < 16 || rc_param.frame_height > 16384) {
    error_message << "frame_width (" << rc_param.frame_width
                  << ") and frame_height (" << rc_param.frame_height
                  << ") must be in the range [16, 16384].";
    return { AOM_CODEC_INVALID_PARAM, error_message.str() };
  }
  rc_param_ = rc_param;
  return { AOM_CODEC_OK, "" };
}

// Threshold for use of the lagging second reference frame. High second ref
// usage may point to a transient event like a flash or occlusion rather than
// a real scene cut.
// We adapt the threshold based on number of frames in this key-frame group so
// far.
static double GetSecondRefUsageThreshold(int frame_count_so_far) {
  const int adapt_upto = 32;
  const double min_second_ref_usage_thresh = 0.085;
  const double second_ref_usage_thresh_max_delta = 0.035;
  if (frame_count_so_far >= adapt_upto) {
    return min_second_ref_usage_thresh + second_ref_usage_thresh_max_delta;
  }
  return min_second_ref_usage_thresh +
         ((double)frame_count_so_far / (adapt_upto - 1)) *
             second_ref_usage_thresh_max_delta;
}

// Slide show transition detection.
// Tests for case where there is very low error either side of the current frame
// but much higher just for this frame. This can help detect key frames in
// slide shows even where the slides are pictures of different sizes.
// Also requires that intra and inter errors are very similar to help eliminate
// harmful false positives.
// It will not help if the transition is a fade or other multi-frame effect.
static bool DetectSlideTransition(const FIRSTPASS_STATS &this_frame,
                                  const FIRSTPASS_STATS &last_frame,
                                  const FIRSTPASS_STATS &next_frame) {
  // Intra / Inter threshold very low
  constexpr double kVeryLowII = 1.5;
  // Clean slide transitions we expect a sharp single frame spike in error.
  constexpr double kErrorSpike = 5.0;

  // TODO(angiebird): Understand the meaning of these conditions.
  return (this_frame.intra_error < (this_frame.coded_error * kVeryLowII)) &&
         (this_frame.coded_error > (last_frame.coded_error * kErrorSpike)) &&
         (this_frame.coded_error > (next_frame.coded_error * kErrorSpike));
}

// Check if there is a significant intra/inter error change between the current
// frame and its neighbor. If so, we should further test whether the current
// frame should be a key frame.
static bool DetectIntraInterErrorChange(const FIRSTPASS_STATS &this_stats,
                                        const FIRSTPASS_STATS &last_stats,
                                        const FIRSTPASS_STATS &next_stats) {
  // Minimum % intra coding observed in first pass (1.0 = 100%)
  constexpr double kMinIntraLevel = 0.25;
  // Minimum ratio between the % of intra coding and inter coding in the first
  // pass after discounting neutral blocks (discounting neutral blocks in this
  // way helps catch scene cuts in clips with very flat areas or letter box
  // format clips with image padding.
  constexpr double kIntraVsInterRatio = 2.0;

  const double modified_pcnt_inter =
      this_stats.pcnt_inter - this_stats.pcnt_neutral;
  const double pcnt_intra_min =
      std::max(kMinIntraLevel, kIntraVsInterRatio * modified_pcnt_inter);

  // In real scene cuts there is almost always a sharp change in the intra
  // or inter error score.
  constexpr double kErrorChangeThreshold = 0.4;
  const double last_this_error_ratio =
      fabs(last_stats.coded_error - this_stats.coded_error) /
      ModifyDivisor(this_stats.coded_error);

  const double this_next_error_ratio =
      fabs(last_stats.intra_error - this_stats.intra_error) /
      ModifyDivisor(this_stats.intra_error);

  // Maximum threshold for the relative ratio of intra error score vs best
  // inter error score.
  constexpr double kThisIntraCodedErrorRatioMax = 1.9;
  const double this_intra_coded_error_ratio =
      this_stats.intra_error / ModifyDivisor(this_stats.coded_error);

  // For real scene cuts we expect an improvment in the intra inter error
  // ratio in the next frame.
  constexpr double kNextIntraCodedErrorRatioMin = 3.5;
  const double next_intra_coded_error_ratio =
      next_stats.intra_error / ModifyDivisor(next_stats.coded_error);

  double pcnt_intra = 1.0 - this_stats.pcnt_inter;
  return pcnt_intra > pcnt_intra_min &&
         this_intra_coded_error_ratio < kThisIntraCodedErrorRatioMax &&
         (last_this_error_ratio > kErrorChangeThreshold ||
          this_next_error_ratio > kErrorChangeThreshold ||
          next_intra_coded_error_ratio > kNextIntraCodedErrorRatioMin);
}

// Check whether the candidate can be a key frame.
// This is a rewrite of test_candidate_kf().
static bool TestCandidateKey(const FirstpassInfo &first_pass_info,
                             int candidate_key_idx, int frames_since_prev_key) {
  const auto &stats_list = first_pass_info.stats_list;
  const int stats_count = static_cast<int>(stats_list.size());
  if (candidate_key_idx + 1 >= stats_count || candidate_key_idx - 1 < 0) {
    return false;
  }
  const auto &last_stats = stats_list[candidate_key_idx - 1];
  const auto &this_stats = stats_list[candidate_key_idx];
  const auto &next_stats = stats_list[candidate_key_idx + 1];

  if (frames_since_prev_key < 3) return false;
  const double second_ref_usage_threshold =
      GetSecondRefUsageThreshold(frames_since_prev_key);
  if (this_stats.pcnt_second_ref >= second_ref_usage_threshold) return false;
  if (next_stats.pcnt_second_ref >= second_ref_usage_threshold) return false;

  // Hard threshold where the first pass chooses intra for almost all blocks.
  // In such a case even if the frame is not a scene cut coding a key frame
  // may be a good option.
  constexpr double kVeryLowInterThreshold = 0.05;
  if (this_stats.pcnt_inter < kVeryLowInterThreshold ||
      DetectSlideTransition(this_stats, last_stats, next_stats) ||
      DetectIntraInterErrorChange(this_stats, last_stats, next_stats)) {
    double boost_score = 0.0;
    double decay_accumulator = 1.0;

    // We do "-1" because the candidate key is not counted.
    int stats_after_this_stats = stats_count - candidate_key_idx - 1;

    // Number of frames required to test for scene cut detection
    constexpr int kSceneCutKeyTestIntervalMax = 16;

    // Make sure we have enough stats after the candidate key.
    const int frames_to_test_after_candidate_key =
        std::min(kSceneCutKeyTestIntervalMax, stats_after_this_stats);

    // Examine how well the key frame predicts subsequent frames.
    int i;
    for (i = 1; i <= frames_to_test_after_candidate_key; ++i) {
      // Get the next frame details
      const auto &stats = stats_list[candidate_key_idx + i];

      // Cumulative effect of decay in prediction quality.
      if (stats.pcnt_inter > 0.85) {
        decay_accumulator *= stats.pcnt_inter;
      } else {
        decay_accumulator *= (0.85 + stats.pcnt_inter) / 2.0;
      }

      constexpr double kBoostFactor = 12.5;
      double next_iiratio =
          (kBoostFactor * stats.intra_error / ModifyDivisor(stats.coded_error));
      next_iiratio = std::min(next_iiratio, 128.0);
      double boost_score_increment = decay_accumulator * next_iiratio;

      // Keep a running total.
      boost_score += boost_score_increment;

      // Test various breakout clauses.
      // TODO(any): Test of intra error should be normalized to an MB.
      // TODO(angiebird): Investigate the following questions.
      // Question 1: next_iiratio (intra_error / coded_error) * kBoostFactor
      // We know intra_error / coded_error >= 1 and kBoostFactor = 12.5,
      // therefore, (intra_error / coded_error) * kBoostFactor will always
      // greater than 1.5. Is "next_iiratio < 1.5" always false?
      // Question 2: Similar to question 1, is "next_iiratio < 3.0" always true?
      // Question 3: Why do we need to divide 200 with num_mbs_16x16?
      if ((stats.pcnt_inter < 0.05) || (next_iiratio < 1.5) ||
          (((stats.pcnt_inter - stats.pcnt_neutral) < 0.20) &&
           (next_iiratio < 3.0)) ||
          (boost_score_increment < 3.0) ||
          (stats.intra_error <
           (200.0 / static_cast<double>(first_pass_info.num_mbs_16x16)))) {
        break;
      }
    }

    // If there is tolerable prediction for at least the next 3 frames then
    // break out else discard this potential key frame and move on
    const int count_for_tolerable_prediction = 3;
    if (boost_score > 30.0 && (i > count_for_tolerable_prediction)) {
      return true;
    }
  }
  return false;
}

// Compute key frame location from first_pass_info.
std::vector<int> GetKeyFrameList(const FirstpassInfo &first_pass_info) {
  std::vector<int> key_frame_list;
  key_frame_list.push_back(0);  // The first frame is always a key frame
  int candidate_key_idx = 1;
  while (candidate_key_idx <
         static_cast<int>(first_pass_info.stats_list.size())) {
    const int frames_since_prev_key = candidate_key_idx - key_frame_list.back();
    // Check for a scene cut.
    const bool scenecut_detected = TestCandidateKey(
        first_pass_info, candidate_key_idx, frames_since_prev_key);
    if (scenecut_detected) {
      key_frame_list.push_back(candidate_key_idx);
    }
    ++candidate_key_idx;
  }
  return key_frame_list;
}

// initialize GF_GROUP_STATS
static void InitGFStats(GF_GROUP_STATS *gf_stats) {
  gf_stats->gf_group_err = 0.0;
  gf_stats->gf_group_raw_error = 0.0;
  gf_stats->gf_group_skip_pct = 0.0;
  gf_stats->gf_group_inactive_zone_rows = 0.0;

  gf_stats->mv_ratio_accumulator = 0.0;
  gf_stats->decay_accumulator = 1.0;
  gf_stats->zero_motion_accumulator = 1.0;
  gf_stats->loop_decay_rate = 1.0;
  gf_stats->last_loop_decay_rate = 1.0;
  gf_stats->this_frame_mv_in_out = 0.0;
  gf_stats->mv_in_out_accumulator = 0.0;
  gf_stats->abs_mv_in_out_accumulator = 0.0;

  gf_stats->avg_sr_coded_error = 0.0;
  gf_stats->avg_pcnt_second_ref = 0.0;
  gf_stats->avg_new_mv_count = 0.0;
  gf_stats->avg_wavelet_energy = 0.0;
  gf_stats->avg_raw_err_stdev = 0.0;
  gf_stats->non_zero_stdev_count = 0;
}

static int FindRegionIndex(const std::vector<REGIONS> &regions, int frame_idx) {
  for (int k = 0; k < static_cast<int>(regions.size()); k++) {
    if (regions[k].start <= frame_idx && regions[k].last >= frame_idx) {
      return k;
    }
  }
  return -1;
}

// This function detects a flash through the high relative pcnt_second_ref
// score in the frame following a flash frame. The offset passed in should
// reflect this.
static bool DetectFlash(const std::vector<FIRSTPASS_STATS> &stats_list,
                        int index) {
  int next_index = index + 1;
  if (next_index >= static_cast<int>(stats_list.size())) return false;
  const FIRSTPASS_STATS &next_frame = stats_list[next_index];

  // What we are looking for here is a situation where there is a
  // brief break in prediction (such as a flash) but subsequent frames
  // are reasonably well predicted by an earlier (pre flash) frame.
  // The recovery after a flash is indicated by a high pcnt_second_ref
  // compared to pcnt_inter.
  return next_frame.pcnt_second_ref > next_frame.pcnt_inter &&
         next_frame.pcnt_second_ref >= 0.5;
}

#define MIN_SHRINK_LEN 6

// This function takes in a suggesting gop interval from cur_start to cur_last,
// analyzes firstpass stats and region stats and then return a better gop cut
// location.
// TODO(b/231517281): Simplify the indices once we have an unit test.
// We are using four indices here, order_index, cur_start, cur_last, and
// frames_since_key. Ideally, only three indices are needed.
// 1) start_index = order_index + cur_start
// 2) end_index = order_index + cur_end
// 3) key_index
int FindBetterGopCut(const std::vector<FIRSTPASS_STATS> &stats_list,
                     const std::vector<REGIONS> &regions_list,
                     int min_gop_show_frame_count, int max_gop_show_frame_count,
                     int order_index, int cur_start, int cur_last,
                     int frames_since_key) {
  // only try shrinking if interval smaller than active_max_gf_interval
  if (cur_last - cur_start > max_gop_show_frame_count ||
      cur_start >= cur_last) {
    return cur_last;
  }
  int num_regions = static_cast<int>(regions_list.size());
  int num_stats = static_cast<int>(stats_list.size());
  const int min_shrink_int = std::max(MIN_SHRINK_LEN, min_gop_show_frame_count);

  // find the region indices of where the first and last frame belong.
  int k_start = FindRegionIndex(regions_list, cur_start + frames_since_key);
  int k_last = FindRegionIndex(regions_list, cur_last + frames_since_key);
  if (cur_start + frames_since_key == 0) k_start = 0;

  int scenecut_idx = -1;
  // See if we have a scenecut in between
  for (int r = k_start + 1; r <= k_last; r++) {
    if (regions_list[r].type == SCENECUT_REGION &&
        regions_list[r].last - frames_since_key - cur_start >
            min_gop_show_frame_count) {
      scenecut_idx = r;
      break;
    }
  }

  // if the found scenecut is very close to the end, ignore it.
  if (scenecut_idx >= 0 &&
      regions_list[num_regions - 1].last - regions_list[scenecut_idx].last <
          4) {
    scenecut_idx = -1;
  }

  if (scenecut_idx != -1) {
    // If we have a scenecut, then stop at it.
    // TODO(bohanli): add logic here to stop before the scenecut and for
    // the next gop start from the scenecut with GF
    int is_minor_sc =
        (regions_list[scenecut_idx].avg_cor_coeff *
             (1 - stats_list[order_index + regions_list[scenecut_idx].start -
                             frames_since_key]
                          .noise_var /
                      regions_list[scenecut_idx].avg_intra_err) >
         0.6);
    cur_last =
        regions_list[scenecut_idx].last - frames_since_key - !is_minor_sc;
  } else {
    int is_last_analysed =
        (k_last == num_regions - 1) &&
        (cur_last + frames_since_key == regions_list[k_last].last);
    int not_enough_regions =
        k_last - k_start <= 1 + (regions_list[k_start].type == SCENECUT_REGION);
    // if we are very close to the end, then do not shrink since it may
    // introduce intervals that are too short
    if (!(is_last_analysed && not_enough_regions)) {
      const double arf_length_factor = 0.1;
      double best_score = 0;
      int best_j = -1;
      const int first_frame = regions_list[0].start - frames_since_key;
      const int last_frame =
          regions_list[num_regions - 1].last - frames_since_key;
      // score of how much the arf helps the whole GOP
      double base_score = 0.0;
      // Accumulate base_score in
      for (int j = cur_start + 1; j < cur_start + min_shrink_int; j++) {
        if (order_index + j >= num_stats) break;
        base_score = (base_score + 1.0) * stats_list[order_index + j].cor_coeff;
      }
      int met_blending = 0;   // Whether we have met blending areas before
      int last_blending = 0;  // Whether the previous frame if blending
      for (int j = cur_start + min_shrink_int; j <= cur_last; j++) {
        if (order_index + j >= num_stats) break;
        base_score = (base_score + 1.0) * stats_list[order_index + j].cor_coeff;
        int this_reg = FindRegionIndex(regions_list, j + frames_since_key);
        if (this_reg < 0) continue;
        // A GOP should include at most 1 blending region.
        if (regions_list[this_reg].type == BLENDING_REGION) {
          last_blending = 1;
          if (met_blending) {
            break;
          } else {
            base_score = 0;
            continue;
          }
        } else {
          if (last_blending) met_blending = 1;
          last_blending = 0;
        }

        // Add the factor of how good the neighborhood is for this
        // candidate arf.
        double this_score = arf_length_factor * base_score;
        double temp_accu_coeff = 1.0;
        // following frames
        int count_f = 0;
        for (int n = j + 1; n <= j + 3 && n <= last_frame; n++) {
          if (order_index + n >= num_stats) break;
          temp_accu_coeff *= stats_list[order_index + n].cor_coeff;
          this_score +=
              temp_accu_coeff *
              sqrt(AOMMAX(
                  0.5, 1 - stats_list[order_index + n].noise_var /
                               AOMMAX(stats_list[order_index + n].intra_error,
                                      0.001)));
          count_f++;
        }
        // preceding frames
        temp_accu_coeff = 1.0;
        for (int n = j; n > j - 3 * 2 + count_f && n > first_frame; n--) {
          if (order_index + n < 0) break;
          temp_accu_coeff *= stats_list[order_index + n].cor_coeff;
          this_score +=
              temp_accu_coeff *
              sqrt(AOMMAX(
                  0.5, 1 - stats_list[order_index + n].noise_var /
                               AOMMAX(stats_list[order_index + n].intra_error,
                                      0.001)));
        }

        if (this_score > best_score) {
          best_score = this_score;
          best_j = j;
        }
      }

      // For blending areas, move one more frame in case we missed the
      // first blending frame.
      int best_reg = FindRegionIndex(regions_list, best_j + frames_since_key);
      if (best_reg < num_regions - 1 && best_reg > 0) {
        if (regions_list[best_reg - 1].type == BLENDING_REGION &&
            regions_list[best_reg + 1].type == BLENDING_REGION) {
          if (best_j + frames_since_key == regions_list[best_reg].start &&
              best_j + frames_since_key < regions_list[best_reg].last) {
            best_j += 1;
          } else if (best_j + frames_since_key == regions_list[best_reg].last &&
                     best_j + frames_since_key > regions_list[best_reg].start) {
            best_j -= 1;
          }
        }
      }

      if (cur_last - best_j < 2) best_j = cur_last;
      if (best_j > 0 && best_score > 0.1) cur_last = best_j;
      // if cannot find anything, just cut at the original place.
    }
  }

  return cur_last;
}

// Function to test for a condition where a complex transition is followed
// by a static section. For example in slide shows where there is a fade
// between slides. This is to help with more optimal kf and gf positioning.
static bool DetectTransitionToStill(
    const std::vector<FIRSTPASS_STATS> &stats_list, int next_stats_index,
    int min_gop_show_frame_count, int frame_interval, int still_interval,
    double loop_decay_rate, double last_decay_rate) {
  // Break clause to detect very still sections after motion
  // For example a static image after a fade or other transition
  // instead of a clean scene cut.
  if (frame_interval > min_gop_show_frame_count && loop_decay_rate >= 0.999 &&
      last_decay_rate < 0.9) {
    int stats_count = static_cast<int>(stats_list.size());
    int stats_left = stats_count - next_stats_index;
    if (stats_left >= still_interval) {
      // Look ahead a few frames to see if static condition persists...
      int j;
      for (j = 0; j < still_interval; ++j) {
        const FIRSTPASS_STATS &stats = stats_list[next_stats_index + j];
        if (stats.pcnt_inter - stats.pcnt_motion < 0.999) break;
      }
      // Only if it does do we signal a transition to still.
      return j == still_interval;
    }
  }
  return false;
}

static int DetectGopCut(const std::vector<FIRSTPASS_STATS> &stats_list,
                        int start_idx, int candidate_cut_idx, int next_key_idx,
                        int flash_detected, int min_gop_show_frame_count,
                        int max_gop_show_frame_count, int frame_width,
                        int frame_height, const GF_GROUP_STATS &gf_stats) {
  (void)max_gop_show_frame_count;
  const int candidate_gop_size = candidate_cut_idx - start_idx;

  if (!flash_detected) {
    // Break clause to detect very still sections after motion. For example,
    // a static image after a fade or other transition.
    if (DetectTransitionToStill(stats_list, start_idx, min_gop_show_frame_count,
                                candidate_gop_size, 5, gf_stats.loop_decay_rate,
                                gf_stats.last_loop_decay_rate)) {
      return 1;
    }
    const double arf_abs_zoom_thresh = 4.4;
    // Motion breakout threshold for loop below depends on image size.
    const double mv_ratio_accumulator_thresh =
        (frame_height + frame_width) / 4.0;
    // Some conditions to breakout after min interval.
    if (candidate_gop_size >= min_gop_show_frame_count &&
        // If possible don't break very close to a kf
        (next_key_idx - candidate_cut_idx >= min_gop_show_frame_count) &&
        (candidate_gop_size & 0x01) &&
        (gf_stats.mv_ratio_accumulator > mv_ratio_accumulator_thresh ||
         gf_stats.abs_mv_in_out_accumulator > arf_abs_zoom_thresh)) {
      return 1;
    }
  }

  // TODO(b/231489624): Check if we need this part.
  // If almost totally static, we will not use the the max GF length later,
  // so we can continue for more frames.
  // if ((candidate_gop_size >= active_max_gf_interval + 1) &&
  //     !is_almost_static(gf_stats->zero_motion_accumulator,
  //                       twopass->kf_zeromotion_pct, cpi->ppi->lap_enabled)) {
  //   return 0;
  // }
  return 0;
}

/*!\brief Determine the length of future GF groups.
 *
 * \ingroup gf_group_algo
 * This function decides the gf group length of future frames in batch
 *
 * \param[in]    rc_param         Rate control parameters
 * \param[in]    stats_list       List of first pass stats
 * \param[in]    regions_list     List of regions from av1_identify_regions
 * \param[in]    order_index      Index of current frame in stats_list
 * \param[in]    frames_since_key Number of frames since the last key frame
 * \param[in]    frames_to_key    Number of frames to the next key frame
 *
 * \return Returns a vector of decided GF group lengths.
 */
static std::vector<int> PartitionGopIntervals(
    const RateControlParam &rc_param,
    const std::vector<FIRSTPASS_STATS> &stats_list,
    const std::vector<REGIONS> &regions_list, int order_index,
    int frames_since_key, int frames_to_key) {
  int i = 0;
  // If cpi->gf_state.arf_gf_boost_lst is 0, we are starting with a KF or GF.
  int cur_start = 0;
  // Each element is the last frame of the previous GOP. If there are n GOPs,
  // you need n + 1 cuts to find the durations. So cut_pos starts out with -1,
  // which is the last frame of the previous GOP.
  std::vector<int> cut_pos(1, -1);
  int cut_here = 0;
  GF_GROUP_STATS gf_stats;
  InitGFStats(&gf_stats);
  int num_stats = static_cast<int>(stats_list.size());

  while (i + order_index < num_stats) {
    // reaches next key frame, break here
    if (i >= frames_to_key - 1) {
      cut_here = 2;
    } else if (i - cur_start >= rc_param.max_gop_show_frame_count) {
      // reached maximum len, but nothing special yet (almost static)
      // let's look at the next interval
      cut_here = 2;
    } else {
      // Test for the case where there is a brief flash but the prediction
      // quality back to an earlier frame is then restored.
      const int gop_start_idx = cur_start + order_index;
      const int candidate_gop_cut_idx = i + order_index;
      const int next_key_idx = frames_to_key + order_index;
      const bool flash_detected =
          DetectFlash(stats_list, candidate_gop_cut_idx);

      // TODO(bohanli): remove redundant accumulations here, or unify
      // this and the ones in define_gf_group
      const FIRSTPASS_STATS *stats = &stats_list[candidate_gop_cut_idx];
      av1_accumulate_next_frame_stats(stats, flash_detected, frames_since_key,
                                      i, &gf_stats, rc_param.frame_width,
                                      rc_param.frame_height);

      // TODO(angiebird): Can we simplify this part? Looks like we are going to
      // change the gop cut index with FindBetterGopCut() anyway.
      cut_here = DetectGopCut(
          stats_list, gop_start_idx, candidate_gop_cut_idx, next_key_idx,
          flash_detected, rc_param.min_gop_show_frame_count,
          rc_param.max_gop_show_frame_count, rc_param.frame_width,
          rc_param.frame_height, gf_stats);
    }

    if (!cut_here) {
      ++i;
      continue;
    }

    // the current last frame in the gf group
    int original_last = cut_here > 1 ? i : i - 1;
    int cur_last = FindBetterGopCut(
        stats_list, regions_list, rc_param.min_gop_show_frame_count,
        rc_param.max_gop_show_frame_count, order_index, cur_start,
        original_last, frames_since_key);
    // only try shrinking if interval smaller than active_max_gf_interval
    cut_pos.push_back(cur_last);

    // reset pointers to the shrunken location
    cur_start = cur_last;
    int cur_region_idx =
        FindRegionIndex(regions_list, cur_start + 1 + frames_since_key);
    if (cur_region_idx >= 0)
      if (regions_list[cur_region_idx].type == SCENECUT_REGION) cur_start++;

    // reset accumulators
    InitGFStats(&gf_stats);
    i = cur_last + 1;

    if (cut_here == 2 && i >= frames_to_key) break;
  }

  std::vector<int> gf_intervals;
  // save intervals
  for (size_t n = 1; n < cut_pos.size(); n++) {
    gf_intervals.push_back(cut_pos[n] - cut_pos[n - 1]);
  }

  return gf_intervals;
}

// Make a copy of the first pass stats, and analyze them
FirstpassInfo AnalyzeFpStats(FirstpassInfo firstpass_info) {
  const int stats_size = static_cast<int>(firstpass_info.stats_list.size());
  av1_mark_flashes(firstpass_info.stats_list.data(),
                   firstpass_info.stats_list.data() + stats_size);
  av1_estimate_noise(firstpass_info.stats_list.data(),
                     firstpass_info.stats_list.data() + stats_size);
  av1_estimate_coeff(firstpass_info.stats_list.data(),
                     firstpass_info.stats_list.data() + stats_size);
  return firstpass_info;
}

StatusOr<GopStructList> AV1RateControlQMode::DetermineGopInfo(
    const FirstpassInfo &firstpass_info) {
  const int stats_size = static_cast<int>(firstpass_info.stats_list.size());
  if (stats_size <= 0) {
    Status status;
    status.code = AOM_CODEC_INVALID_PARAM;
    status.message = "The firstpass info length is insufficient.";
    return status;
  }
  GopStructList gop_list;
  RefFrameManager ref_frame_manager(rc_param_.ref_frame_table_size,
                                    rc_param_.max_ref_frames);

  const FirstpassInfo analyzed_fp_info =
      AnalyzeFpStats(std::move(firstpass_info));

  // Calculate the average coded error of the whole sequence
  double global_avg_coded_error = 0.0;
  for (int i = 0; i < stats_size; ++i) {
    global_avg_coded_error +=
        log(1.0 + std::min(analyzed_fp_info.stats_list[i].coded_error,
                           analyzed_fp_info.stats_list[i].sr_coded_error));
  }
  global_avg_coded_error /= static_cast<double>(stats_size);

  int global_coding_idx_offset = 0;
  int global_order_idx_offset = 0;
  std::vector<int> key_frame_list = GetKeyFrameList(analyzed_fp_info);
  key_frame_list.push_back(stats_size);  // a sentinel value
  for (size_t ki = 0; ki + 1 < key_frame_list.size(); ++ki) {
    int frames_to_key = key_frame_list[ki + 1] - key_frame_list[ki];
    int key_order_index = key_frame_list[ki];  // The key frame's display order

    std::vector<REGIONS> regions_list(MAX_FIRSTPASS_ANALYSIS_FRAMES);
    int total_regions = 0;
    av1_identify_regions(analyzed_fp_info.stats_list.data() + key_order_index,
                         frames_to_key, 0, regions_list.data(), &total_regions);
    regions_list.resize(total_regions);
    std::vector<int> gf_intervals = PartitionGopIntervals(
        rc_param_, analyzed_fp_info.stats_list, regions_list, key_order_index,
        /*frames_since_key=*/0, frames_to_key);
    bool use_prev_arf = false;
    for (size_t gi = 0; gi < gf_intervals.size(); ++gi) {
      const bool has_key_frame = gi == 0;
      const int show_frame_count = gf_intervals[gi];
      const bool has_arf_frame = show_frame_count > kMinIntervalToAddArf;

      // Calculate the average coded error in the GOP to determine base_q_ratio
      double gop_avg_coded_error = 0.0;
      for (int i = global_order_idx_offset;
           i < global_order_idx_offset + show_frame_count; ++i) {
        gop_avg_coded_error +=
            log(1.0 + std::min(analyzed_fp_info.stats_list[i].coded_error,
                               analyzed_fp_info.stats_list[i].sr_coded_error));
      }
      gop_avg_coded_error /=
          std::max(static_cast<double>(show_frame_count), 1.0);
      double base_q_ratio =
          fabs(global_avg_coded_error - gop_avg_coded_error) < 0.001
              ? 1.0
              : exp((global_avg_coded_error - gop_avg_coded_error) * 2.0);

      GopStruct gop =
          ConstructGop(&ref_frame_manager, show_frame_count, has_key_frame,
                       has_arf_frame, use_prev_arf, global_coding_idx_offset,
                       global_order_idx_offset, base_q_ratio);
      assert(gop.show_frame_count == show_frame_count);
      global_coding_idx_offset += static_cast<int>(gop.gop_frame_list.size());
      global_order_idx_offset += gop.show_frame_count;
      gop_list.push_back(gop);
      // If there's no arf in this GOP, note it down for the next GOP
      use_prev_arf = has_arf_frame;
    }
  }
  return gop_list;
}

TplFrameDepStats CreateTplFrameDepStats(int frame_height, int frame_width,
                                        int min_block_size,
                                        bool has_alt_stats) {
  const int unit_rows = (frame_height + min_block_size - 1) / min_block_size;
  const int unit_cols = (frame_width + min_block_size - 1) / min_block_size;
  TplFrameDepStats frame_dep_stats;
  frame_dep_stats.unit_size = min_block_size;
  frame_dep_stats.unit_stats.resize(unit_rows);
  for (auto &row : frame_dep_stats.unit_stats) {
    row.resize(unit_cols);
  }

  if (has_alt_stats) {
    frame_dep_stats.alt_unit_stats.resize(unit_rows);
    for (auto &row : frame_dep_stats.alt_unit_stats) {
      row.resize(unit_cols);
    }
  }
  return frame_dep_stats;
}

TplUnitDepStats TplBlockStatsToDepStats(const TplBlockStats &block_stats,
                                        int unit_count,
                                        bool rate_dist_present) {
  TplUnitDepStats dep_stats = {};
  if (rate_dist_present) {
    dep_stats.intra_cost = block_stats.intra_pred_err * 1.0 / unit_count;
    dep_stats.inter_cost = block_stats.inter_pred_err * 1.0 / unit_count;
  } else {
    dep_stats.intra_cost = block_stats.intra_cost * 1.0 / unit_count;
    dep_stats.inter_cost = block_stats.inter_cost * 1.0 / unit_count;
  }
  // In rare case, inter_cost may be greater than intra_cost.
  // If so, we need to modify inter_cost such that inter_cost <= intra_cost
  // because it is required by GetPropagationFraction()
  if (block_stats.ref_frame_index[0] >= 0)
    dep_stats.inter_cost = std::min(dep_stats.intra_cost, dep_stats.inter_cost);
  else
    dep_stats.inter_cost = dep_stats.intra_cost;
  dep_stats.mv = block_stats.mv;
  dep_stats.ref_frame_index = block_stats.ref_frame_index;
  return dep_stats;
}

namespace {
Status ValidateBlockStats(const TplFrameStats &frame_stats,
                          const TplBlockStats &block_stats,
                          int min_block_size) {
  if (block_stats.col >= frame_stats.frame_width ||
      block_stats.row >= frame_stats.frame_height) {
    std::ostringstream error_message;
    error_message << "Block position (" << block_stats.col << ", "
                  << block_stats.row
                  << ") is out of range; frame dimensions are "
                  << frame_stats.frame_width << " x "
                  << frame_stats.frame_height;
    return { AOM_CODEC_INVALID_PARAM, error_message.str() };
  }
  if (block_stats.col % min_block_size != 0 ||
      block_stats.row % min_block_size != 0 ||
      block_stats.width % min_block_size != 0 ||
      block_stats.height % min_block_size != 0) {
    std::ostringstream error_message;
    error_message
        << "Invalid block position or dimension, must be a multiple of "
        << min_block_size << "; col = " << block_stats.col
        << ", row = " << block_stats.row << ", width = " << block_stats.width
        << ", height = " << block_stats.height;
    return { AOM_CODEC_INVALID_PARAM, error_message.str() };
  }
  return { AOM_CODEC_OK, "" };
}

Status ValidateTplStats(const GopStruct &gop_struct,
                        const TplGopStats &tpl_gop_stats) {
  constexpr char kAdvice[] =
      "Do the current RateControlParam settings match those used to generate "
      "the TPL stats?";
  if (gop_struct.gop_frame_list.size() !=
      tpl_gop_stats.frame_stats_list.size()) {
    std::ostringstream error_message;
    error_message << "Frame count of GopStruct ("
                  << gop_struct.gop_frame_list.size()
                  << ") doesn't match frame count of TPL stats ("
                  << tpl_gop_stats.frame_stats_list.size() << "). " << kAdvice;
    return { AOM_CODEC_INVALID_PARAM, error_message.str() };
  }
  for (int i = 0; i < static_cast<int>(gop_struct.gop_frame_list.size()); ++i) {
    const bool is_ref_frame = gop_struct.gop_frame_list[i].update_ref_idx >= 0;
    const bool has_tpl_stats =
        !tpl_gop_stats.frame_stats_list[i].block_stats_list.empty();
    if (is_ref_frame && !has_tpl_stats) {
      std::ostringstream error_message;
      error_message << "The frame with global_coding_idx "
                    << gop_struct.gop_frame_list[i].global_coding_idx
                    << " is a reference frame, but has no TPL stats. "
                    << kAdvice;
      return { AOM_CODEC_INVALID_PARAM, error_message.str() };
    }
  }
  return { AOM_CODEC_OK, "" };
}
}  // namespace

Status FillTplUnitDepStats(
    std::vector<std::vector<TplUnitDepStats>> &unit_stats,
    const TplFrameStats &frame_stats,
    const std::vector<TplBlockStats> &block_stats_list) {
  const int min_block_size = frame_stats.min_block_size;
  const int unit_rows =
      (frame_stats.frame_height + min_block_size - 1) / min_block_size;
  const int unit_cols =
      (frame_stats.frame_width + min_block_size - 1) / min_block_size;
  for (const TplBlockStats &block_stats : block_stats_list) {
    Status status =
        ValidateBlockStats(frame_stats, block_stats, min_block_size);
    if (!status.ok()) {
      return status;
    }
    const int block_unit_row = block_stats.row / min_block_size;
    const int block_unit_col = block_stats.col / min_block_size;
    // The block must start within the frame boundaries, but it may extend past
    // the right edge or bottom of the frame. Find the number of unit rows and
    // columns in the block which are fully within the frame.
    const int block_unit_rows = std::min(block_stats.height / min_block_size,
                                         unit_rows - block_unit_row);
    const int block_unit_cols = std::min(block_stats.width / min_block_size,
                                         unit_cols - block_unit_col);
    const int unit_count = block_unit_rows * block_unit_cols;
    TplUnitDepStats this_unit_stats = TplBlockStatsToDepStats(
        block_stats, unit_count, frame_stats.rate_dist_present);
    for (int r = 0; r < block_unit_rows; r++) {
      for (int c = 0; c < block_unit_cols; c++) {
        unit_stats[block_unit_row + r][block_unit_col + c] = this_unit_stats;
      }
    }
  }
  return { AOM_CODEC_OK, "" };
}

StatusOr<TplFrameDepStats> CreateTplFrameDepStatsWithoutPropagation(
    const TplFrameStats &frame_stats) {
  if (frame_stats.block_stats_list.empty()) {
    return TplFrameDepStats();
  }
  const int min_block_size = frame_stats.min_block_size;
  TplFrameDepStats frame_dep_stats = CreateTplFrameDepStats(
      frame_stats.frame_height, frame_stats.frame_width, min_block_size,
      !frame_stats.alternate_block_stats_list.empty());

  Status status = FillTplUnitDepStats(frame_dep_stats.unit_stats, frame_stats,
                                      frame_stats.block_stats_list);
  if (!status.ok()) return status;

  if (!frame_stats.alternate_block_stats_list.empty()) {
    status = FillTplUnitDepStats(frame_dep_stats.alt_unit_stats, frame_stats,
                                 frame_stats.alternate_block_stats_list);
    if (!status.ok()) return status;
    frame_dep_stats.rdcost =
        TplFrameDepStatsAccumulateInterCost(frame_dep_stats.unit_stats);
    frame_dep_stats.alt_rdcost =
        TplFrameDepStatsAccumulateInterCost(frame_dep_stats.alt_unit_stats);
  }

  return frame_dep_stats;
}

int GetRefCodingIdxList(const TplUnitDepStats &unit_dep_stats,
                        const RefFrameTable &ref_frame_table,
                        int *ref_coding_idx_list) {
  int ref_frame_count = 0;
  for (int i = 0; i < kBlockRefCount; ++i) {
    ref_coding_idx_list[i] = -1;
    int ref_frame_index = unit_dep_stats.ref_frame_index[i];
    if (ref_frame_index != -1) {
      assert(ref_frame_index < static_cast<int>(ref_frame_table.size()));
      ref_coding_idx_list[i] = ref_frame_table[ref_frame_index].coding_idx;
      ref_frame_count++;
    }
  }
  return ref_frame_count;
}

int GetBlockOverlapArea(int r0, int c0, int r1, int c1, int size) {
  const int r_low = std::max(r0, r1);
  const int r_high = std::min(r0 + size, r1 + size);
  const int c_low = std::max(c0, c1);
  const int c_high = std::min(c0 + size, c1 + size);
  if (r_high >= r_low && c_high >= c_low) {
    return (r_high - r_low) * (c_high - c_low);
  }
  return 0;
}

// TODO(angiebird): Merge TplFrameDepStatsAccumulateIntraCost and
// TplFrameDepStatsAccumulate.
double TplFrameDepStatsAccumulateIntraCost(
    const TplFrameDepStats &frame_dep_stats) {
  auto getIntraCost = [](double sum, const TplUnitDepStats &unit) {
    return sum + unit.intra_cost;
  };
  double sum = 0;
  for (const auto &row : frame_dep_stats.unit_stats) {
    sum = std::accumulate(row.begin(), row.end(), sum, getIntraCost);
  }
  return std::max(sum, 1.0);
}

double TplFrameDepStatsAccumulateInterCost(
    const std::vector<std::vector<TplUnitDepStats>> &unit_stats) {
  auto getInterCost = [](double sum, const TplUnitDepStats &unit) {
    return sum + unit.inter_cost;
  };
  double sum = 0;
  for (const auto &row : unit_stats) {
    sum = std::accumulate(row.begin(), row.end(), sum, getInterCost);
  }
  return std::max(sum, 1.0);
}

double TplFrameDepStatsAccumulate(const TplFrameDepStats &frame_dep_stats) {
  auto getOverallCost = [](double sum, const TplUnitDepStats &unit) {
    return sum + unit.propagation_cost + unit.intra_cost;
  };
  double sum = 0;
  for (const auto &row : frame_dep_stats.unit_stats) {
    sum = std::accumulate(row.begin(), row.end(), sum, getOverallCost);
  }
  return std::max(sum, 1.0);
}

double GetPropagationFraction(const TplUnitDepStats &unit_dep_stats) {
  assert(unit_dep_stats.intra_cost >= unit_dep_stats.inter_cost);
  return (unit_dep_stats.intra_cost - unit_dep_stats.inter_cost) /
         ModifyDivisor(unit_dep_stats.intra_cost);
}

void TplFrameDepStatsBackTrace(int coding_idx, GopFrameType update_type,
                               const RefFrameTable &ref_frame_table,
                               TplGopDepStats *tpl_gop_dep_stats) {
  assert(!tpl_gop_dep_stats->frame_dep_stats_list.empty());
  TplFrameDepStats *frame_dep_stats =
      &tpl_gop_dep_stats->frame_dep_stats_list[coding_idx];

  if (frame_dep_stats->unit_stats.empty()) return;
  if (frame_dep_stats->alt_unit_stats.empty()) return;

  const bool ignore_inter = update_type == GopFrameType::kRegularKey ||
                            update_type == GopFrameType::kRegularArf ||
                            update_type == GopFrameType::kRegularGolden;

  const int unit_size = frame_dep_stats->unit_size;
  const int frame_unit_rows =
      static_cast<int>(frame_dep_stats->unit_stats.size());
  const int frame_unit_cols =
      static_cast<int>(frame_dep_stats->unit_stats[0].size());
  for (int unit_row = 0; unit_row < frame_unit_rows; ++unit_row) {
    for (int unit_col = 0; unit_col < frame_unit_cols; ++unit_col) {
      TplUnitDepStats &unit_dep_stats =
          frame_dep_stats->unit_stats[unit_row][unit_col];
      TplUnitDepStats &alt_unit_dep_stats =
          frame_dep_stats->alt_unit_stats[unit_row][unit_col];

      int ref_coding_idx_list[kBlockRefCount] = { -1, -1 };
      int ref_frame_count = GetRefCodingIdxList(
          alt_unit_dep_stats, ref_frame_table, ref_coding_idx_list);
      if (ref_frame_count == 0) continue;
      MotionVector base_mv[2] = { alt_unit_dep_stats.mv[0],
                                  alt_unit_dep_stats.mv[1] };
      for (int i = 0; i < kBlockRefCount; ++i) {
        if (ref_coding_idx_list[i] == -1) continue;
        assert(
            ref_coding_idx_list[i] <
            static_cast<int>(tpl_gop_dep_stats->frame_dep_stats_list.size()));
        TplFrameDepStats &ref_frame_dep_stats =
            tpl_gop_dep_stats->frame_dep_stats_list[ref_coding_idx_list[i]];
        assert(!ref_frame_dep_stats.alt_unit_stats.empty());
        const auto &mv = base_mv[i];
        const int ref_pixel_r = unit_row * unit_size + mv.row;
        const int ref_pixel_c = unit_col * unit_size + mv.col;
        const int ref_unit_row_low =
            (unit_row * unit_size + mv.row) / unit_size;
        const int ref_unit_col_low =
            (unit_col * unit_size + mv.col) / unit_size;

        for (int j = 0; j < 2; ++j) {
          for (int k = 0; k < 2; ++k) {
            const int ref_unit_row = ref_unit_row_low + j;
            const int ref_unit_col = ref_unit_col_low + k;
            if (ref_unit_row >= 0 && ref_unit_row < frame_unit_rows &&
                ref_unit_col >= 0 && ref_unit_col < frame_unit_cols) {
              const int overlap_area = GetBlockOverlapArea(
                  ref_pixel_r, ref_pixel_c, ref_unit_row * unit_size,
                  ref_unit_col * unit_size, unit_size);
              const double overlap_ratio =
                  overlap_area * 1.0 / (unit_size * unit_size);
              const double propagation_ratio =
                  1.0 / ref_frame_count * overlap_ratio;
              TplUnitDepStats &alt_ref_unit_stats =
                  ref_frame_dep_stats
                      .alt_unit_stats[ref_unit_row][ref_unit_col];
              alt_ref_unit_stats.propagation_cost +=
                  ((ignore_inter ? 0.0 : alt_unit_dep_stats.inter_cost) +
                   alt_unit_dep_stats.propagation_cost) *
                  propagation_ratio;

              TplUnitDepStats &ref_unit_stats =
                  ref_frame_dep_stats.unit_stats[ref_unit_row][ref_unit_col];
              ref_unit_stats.propagation_cost +=
                  ((ignore_inter ? 0.0 : unit_dep_stats.inter_cost) +
                   unit_dep_stats.propagation_cost) *
                  propagation_ratio;
            }
          }
        }
      }
    }
  }
}

void TplFrameDepStatsPropagate(int coding_idx,
                               const RefFrameTable &ref_frame_table,
                               TplGopDepStats *tpl_gop_dep_stats) {
  assert(!tpl_gop_dep_stats->frame_dep_stats_list.empty());
  TplFrameDepStats *frame_dep_stats =
      &tpl_gop_dep_stats->frame_dep_stats_list[coding_idx];

  if (frame_dep_stats->unit_stats.empty()) return;

  const int unit_size = frame_dep_stats->unit_size;
  const int frame_unit_rows =
      static_cast<int>(frame_dep_stats->unit_stats.size());
  const int frame_unit_cols =
      static_cast<int>(frame_dep_stats->unit_stats[0].size());
  for (int unit_row = 0; unit_row < frame_unit_rows; ++unit_row) {
    for (int unit_col = 0; unit_col < frame_unit_cols; ++unit_col) {
      TplUnitDepStats &unit_dep_stats =
          frame_dep_stats->unit_stats[unit_row][unit_col];
      int ref_coding_idx_list[kBlockRefCount] = { -1, -1 };
      int ref_frame_count = GetRefCodingIdxList(unit_dep_stats, ref_frame_table,
                                                ref_coding_idx_list);
      if (ref_frame_count == 0) continue;
      for (int i = 0; i < kBlockRefCount; ++i) {
        if (ref_coding_idx_list[i] == -1) continue;
        assert(
            ref_coding_idx_list[i] <
            static_cast<int>(tpl_gop_dep_stats->frame_dep_stats_list.size()));
        TplFrameDepStats &ref_frame_dep_stats =
            tpl_gop_dep_stats->frame_dep_stats_list[ref_coding_idx_list[i]];
        assert(!ref_frame_dep_stats.unit_stats.empty());
        const auto &mv = unit_dep_stats.mv[i];
        const int ref_pixel_r = unit_row * unit_size + mv.row;
        const int ref_pixel_c = unit_col * unit_size + mv.col;
        const int ref_unit_row_low =
            (unit_row * unit_size + mv.row) / unit_size;
        const int ref_unit_col_low =
            (unit_col * unit_size + mv.col) / unit_size;

        for (int j = 0; j < 2; ++j) {
          for (int k = 0; k < 2; ++k) {
            const int ref_unit_row = ref_unit_row_low + j;
            const int ref_unit_col = ref_unit_col_low + k;
            if (ref_unit_row >= 0 && ref_unit_row < frame_unit_rows &&
                ref_unit_col >= 0 && ref_unit_col < frame_unit_cols) {
              const int overlap_area = GetBlockOverlapArea(
                  ref_pixel_r, ref_pixel_c, ref_unit_row * unit_size,
                  ref_unit_col * unit_size, unit_size);
              const double overlap_ratio =
                  overlap_area * 1.0 / (unit_size * unit_size);
              const double propagation_fraction_power = 0.3;
              // We apply propagation_fraction with power of 0.3 to improve
              // propagation rate. The power coefficient is tuned from empirical
              // practice due to the fact that the inter/intra cost is RD cost
              // not prediction error which is used in the original design. This
              // action will increase the propagation_fraction when the
              // difference between intra_cost and inter_cost are reasonably big
              // but not enough for propagation.
              // TODO(angiebird): Check this part again when we use prediction
              // error for inter/intra cost.
              const double propagation_fraction =
                  std::pow(GetPropagationFraction(unit_dep_stats),
                           propagation_fraction_power);
              const double propagation_ratio =
                  1.0 / ref_frame_count * overlap_ratio;
              TplUnitDepStats &ref_unit_stats =
                  ref_frame_dep_stats.unit_stats[ref_unit_row][ref_unit_col];
              ref_unit_stats.propagation_cost +=
                  (unit_dep_stats.intra_cost - unit_dep_stats.inter_cost +
                   unit_dep_stats.propagation_cost * propagation_fraction) *
                  propagation_ratio;
            }
          }
        }
      }
    }
  }
}

std::vector<RefFrameTable> AV1RateControlQMode::GetRefFrameTableList(
    const GopStruct &gop_struct,
    const std::vector<LookaheadStats> &lookahead_stats,
    RefFrameTable ref_frame_table) {
  if (gop_struct.global_coding_idx_offset == 0) {
    // For the first GOP, ref_frame_table need not be initialized. This is fine,
    // because the first frame (a key frame) will fully initialize it.
    ref_frame_table.assign(rc_param_.ref_frame_table_size, GopFrameInvalid());
  } else {
    // It's not the first GOP, so ref_frame_table must be valid.
    assert(static_cast<int>(ref_frame_table.size()) ==
           rc_param_.ref_frame_table_size);
    assert(std::all_of(ref_frame_table.begin(), ref_frame_table.end(),
                       std::mem_fn(&GopFrame::is_valid)));
    // Reset the frame processing order of the initial ref_frame_table.
    for (GopFrame &gop_frame : ref_frame_table) gop_frame.coding_idx = -1;
  }

  std::vector<RefFrameTable> ref_frame_table_list;
  ref_frame_table_list.push_back(ref_frame_table);
  for (const GopFrame &gop_frame : gop_struct.gop_frame_list) {
    if (gop_frame.is_key_frame) {
      ref_frame_table.assign(rc_param_.ref_frame_table_size, gop_frame);
    } else if (gop_frame.update_ref_idx != -1) {
      assert(gop_frame.update_ref_idx <
             static_cast<int>(ref_frame_table.size()));
      ref_frame_table[gop_frame.update_ref_idx] = gop_frame;
    }
    ref_frame_table_list.push_back(ref_frame_table);
  }

  int gop_size_offset = static_cast<int>(gop_struct.gop_frame_list.size());

  for (const auto &lookahead_stat : lookahead_stats) {
    for (GopFrame gop_frame : lookahead_stat.gop_struct->gop_frame_list) {
      if (gop_frame.is_key_frame) {
        ref_frame_table.assign(rc_param_.ref_frame_table_size, gop_frame);
      } else if (gop_frame.update_ref_idx != -1) {
        assert(gop_frame.update_ref_idx <
               static_cast<int>(ref_frame_table.size()));
        gop_frame.coding_idx += gop_size_offset;
        ref_frame_table[gop_frame.update_ref_idx] = gop_frame;
      }
      ref_frame_table_list.push_back(ref_frame_table);
    }
    gop_size_offset +=
        static_cast<int>(lookahead_stat.gop_struct->gop_frame_list.size());
  }

  return ref_frame_table_list;
}

StatusOr<TplGopDepStats> ComputeTplGopDepStats(
    const GopStruct &gop_struct, const TplGopStats &tpl_gop_stats,
    const std::vector<LookaheadStats> &lookahead_stats,
    const std::vector<RefFrameTable> &ref_frame_table_list) {
  std::vector<const TplFrameStats *> tpl_frame_stats_list_with_lookahead;
  for (const auto &tpl_frame_stats : tpl_gop_stats.frame_stats_list) {
    tpl_frame_stats_list_with_lookahead.push_back(&tpl_frame_stats);
  }
  for (const auto &lookahead_stat : lookahead_stats) {
    for (const auto &tpl_frame_stats :
         lookahead_stat.tpl_gop_stats->frame_stats_list) {
      tpl_frame_stats_list_with_lookahead.push_back(&tpl_frame_stats);
    }
  }

  const int frame_count =
      static_cast<int>(tpl_frame_stats_list_with_lookahead.size());

  // Create the struct to store TPL dependency stats
  TplGopDepStats tpl_gop_dep_stats;

  tpl_gop_dep_stats.frame_dep_stats_list.reserve(frame_count);
  for (int coding_idx = 0; coding_idx < frame_count; coding_idx++) {
    const StatusOr<TplFrameDepStats> tpl_frame_dep_stats =
        CreateTplFrameDepStatsWithoutPropagation(
            *tpl_frame_stats_list_with_lookahead[coding_idx]);
    if (!tpl_frame_dep_stats.ok()) {
      return tpl_frame_dep_stats.status();
    }
    tpl_gop_dep_stats.frame_dep_stats_list.push_back(
        std::move(*tpl_frame_dep_stats));
  }

  // Back propagation
  for (int coding_idx = frame_count - 1; coding_idx >= 0; coding_idx--) {
    auto &ref_frame_table = ref_frame_table_list[coding_idx];
    // TODO(angiebird): Handle/test the case where reference frame
    // is in the previous GOP

    if (tpl_frame_stats_list_with_lookahead[coding_idx]
            ->alternate_block_stats_list.empty()) {
      // One pass TPL run
      TplFrameDepStatsPropagate(coding_idx, ref_frame_table,
                                &tpl_gop_dep_stats);
    } else {
      // Two pass TPL runs

      int first_gop_size = static_cast<int>(gop_struct.gop_frame_list.size());
      GopFrameType update_type;
      if (coding_idx >= first_gop_size) {
        update_type = lookahead_stats[0]
                          .gop_struct[0]
                          .gop_frame_list[coding_idx - first_gop_size]
                          .update_type;
      } else {
        update_type = gop_struct.gop_frame_list[coding_idx].update_type;
      }

      TplFrameDepStatsBackTrace(coding_idx, update_type, ref_frame_table,
                                &tpl_gop_dep_stats);
    }
  }
  return tpl_gop_dep_stats;
}

static double GetFrameImportanceTwoPassTpl(
    const TplFrameDepStats &frame_dep_stats, int frame_width,
    int frame_height) {
  const int sb_size = 64;
  const int num_unit_per_sb = sb_size / frame_dep_stats.unit_size;
  const int sb_rows = (frame_height + sb_size - 1) / sb_size;
  const int sb_cols = (frame_width + sb_size - 1) / sb_size;
  const int unit_rows = (frame_height + frame_dep_stats.unit_size - 1) /
                        frame_dep_stats.unit_size;
  const int unit_cols =
      (frame_width + frame_dep_stats.unit_size - 1) / frame_dep_stats.unit_size;
  std::vector<uint8_t> superblock_q_indices;

  // Cumulate frame level stats
  double cum_inter_cost = 0;
  double cum_rdcost_diff = 0;
  for (int sb_row = 0; sb_row < sb_rows; ++sb_row) {
    for (int sb_col = 0; sb_col < sb_cols; ++sb_col) {
      const int unit_row_start = sb_row * num_unit_per_sb;
      const int unit_row_end =
          std::min((sb_row + 1) * num_unit_per_sb, unit_rows);
      const int unit_col_start = sb_col * num_unit_per_sb;
      const int unit_col_end =
          std::min((sb_col + 1) * num_unit_per_sb, unit_cols);
      // A simplified version of av1_get_q_for_deltaq_objective()
      for (int unit_row = unit_row_start; unit_row < unit_row_end; ++unit_row) {
        for (int unit_col = unit_col_start; unit_col < unit_col_end;
             ++unit_col) {
          const TplUnitDepStats &unit_dep_stats =
              frame_dep_stats.unit_stats[unit_row][unit_col];
          const TplUnitDepStats &alt_unit_dep_stats =
              frame_dep_stats.alt_unit_stats[unit_row][unit_col];
          cum_inter_cost += unit_dep_stats.inter_cost;
          cum_rdcost_diff += std::max(unit_dep_stats.propagation_cost -
                                          alt_unit_dep_stats.propagation_cost,
                                      0.0);
        }
      }
    }
  }
  cum_inter_cost = std::max(cum_inter_cost, 1.0);
  return (cum_rdcost_diff + cum_inter_cost) / cum_inter_cost;
}

static std::vector<uint8_t> SetupDeltaQ(const TplFrameDepStats &frame_dep_stats,
                                        int frame_width, int frame_height,
                                        int base_qindex,
                                        double frame_importance,
                                        bool use_twopass_data) {
  // TODO(jianj) : Add support to various superblock sizes.
  const int sb_size = 64;
  const int delta_q_res = 4;
  const int num_unit_per_sb = sb_size / frame_dep_stats.unit_size;
  const int sb_rows = (frame_height + sb_size - 1) / sb_size;
  const int sb_cols = (frame_width + sb_size - 1) / sb_size;
  const int unit_rows = (frame_height + frame_dep_stats.unit_size - 1) /
                        frame_dep_stats.unit_size;
  const int unit_cols =
      (frame_width + frame_dep_stats.unit_size - 1) / frame_dep_stats.unit_size;
  std::vector<uint8_t> superblock_q_indices;

  // Calculate delta_q offset for each superblock.
  for (int sb_row = 0; sb_row < sb_rows; ++sb_row) {
    for (int sb_col = 0; sb_col < sb_cols; ++sb_col) {
      double intra_cost = 0;
      double mc_dep_cost = 0;
      const int unit_row_start = sb_row * num_unit_per_sb;
      const int unit_row_end =
          std::min((sb_row + 1) * num_unit_per_sb, unit_rows);
      const int unit_col_start = sb_col * num_unit_per_sb;
      const int unit_col_end =
          std::min((sb_col + 1) * num_unit_per_sb, unit_cols);
      // A simplified version of av1_get_q_for_deltaq_objective()
      for (int unit_row = unit_row_start; unit_row < unit_row_end; ++unit_row) {
        for (int unit_col = unit_col_start; unit_col < unit_col_end;
             ++unit_col) {
          const TplUnitDepStats &unit_dep_stats =
              frame_dep_stats.unit_stats[unit_row][unit_col];

          if (use_twopass_data) {
            const TplUnitDepStats &alt_unit_dep_stats =
                frame_dep_stats.alt_unit_stats[unit_row][unit_col];
            mc_dep_cost += std::max(unit_dep_stats.propagation_cost -
                                        alt_unit_dep_stats.propagation_cost,
                                    0.0);
            intra_cost += unit_dep_stats.inter_cost;
          } else {
            mc_dep_cost += unit_dep_stats.propagation_cost;
            intra_cost += unit_dep_stats.intra_cost;
          }
        }
      }
      intra_cost = std::max(intra_cost, 1.0);

      double beta = 1.0;
      const double r0 = 1 / frame_importance;
      const double rk = intra_cost / (mc_dep_cost + intra_cost);
      beta = r0 / rk;
      assert(beta > 0.0);

      int offset = av1_get_deltaq_offset(AOM_BITS_8, base_qindex, beta);
      offset = std::min(offset, delta_q_res * 9 - 1);
      offset = std::max(offset, -delta_q_res * 9 + 1);
      int qindex = offset + base_qindex;
      qindex = std::min(qindex, MAXQ);
      qindex = std::max(qindex, MINQ);
      qindex = av1_adjust_q_from_delta_q_res(delta_q_res, base_qindex, qindex);
      superblock_q_indices.push_back(static_cast<uint8_t>(qindex));
    }
  }

  return superblock_q_indices;
}

static std::unordered_map<int, double> FindKMeansClusterMap(
    const std::vector<uint8_t> &qindices,
    const std::vector<double> &centroids) {
  std::unordered_map<int, double> cluster_map;
  for (const uint8_t qindex : qindices) {
    double nearest_centroid = *std::min_element(
        centroids.begin(), centroids.end(),
        [qindex](const double centroid_a, const double centroid_b) {
          return fabs(centroid_a - qindex) < fabs(centroid_b - qindex);
        });
    cluster_map.insert({ qindex, nearest_centroid });
  }
  return cluster_map;
}

namespace internal {

std::unordered_map<int, int> KMeans(std::vector<uint8_t> qindices, int k) {
  std::vector<double> centroids;
  // Initialize the centroids with first k qindices
  std::unordered_set<int> qindices_set;

  for (const uint8_t qp : qindices) {
    if (!qindices_set.insert(qp).second) continue;  // Already added.
    centroids.push_back(qp);
    if (static_cast<int>(centroids.size()) >= k) break;
  }

  std::unordered_map<int, double> intermediate_cluster_map;
  while (true) {
    // Find the closest centroid for each qindex
    intermediate_cluster_map = FindKMeansClusterMap(qindices, centroids);
    // For each cluster, calculate the new centroids
    std::unordered_map<double, std::vector<int>> centroid_to_qindices;
    for (const auto &qindex_centroid : intermediate_cluster_map) {
      centroid_to_qindices[qindex_centroid.second].push_back(
          qindex_centroid.first);
    }
    bool centroids_changed = false;
    std::vector<double> new_centroids;
    for (const auto &cluster : centroid_to_qindices) {
      double sum = 0.0;
      for (const int qindex : cluster.second) {
        sum += qindex;
      }
      double new_centroid = sum / cluster.second.size();
      new_centroids.push_back(new_centroid);
      if (new_centroid != cluster.first) centroids_changed = true;
    }
    if (!centroids_changed) break;
    centroids = new_centroids;
  }
  std::unordered_map<int, int> cluster_map;
  for (const auto &qindex_centroid : intermediate_cluster_map) {
    cluster_map.insert(
        { qindex_centroid.first, static_cast<int>(qindex_centroid.second) });
  }
  return cluster_map;
}
}  // namespace internal

static int GetRDMult(const GopFrame &gop_frame, int q_index) {
  // TODO(angiebird):
  // 1) Check if these rdmult rules are good in our use case.
  // 2) Support high-bit-depth mode
  if (gop_frame.is_golden_frame) {
    // Assume ARF_UPDATE/GF_UPDATE share the same remult rule.
    return av1_compute_rd_mult_based_on_qindex(AOM_BITS_8, GF_UPDATE, q_index);
  } else if (gop_frame.is_key_frame) {
    return av1_compute_rd_mult_based_on_qindex(AOM_BITS_8, KF_UPDATE, q_index);
  } else {
    // Assume LF_UPDATE/OVERLAY_UPDATE/INTNL_OVERLAY_UPDATE/INTNL_ARF_UPDATE
    // share the same remult rule.
    return av1_compute_rd_mult_based_on_qindex(AOM_BITS_8, LF_UPDATE, q_index);
  }
}

// Check whether a frame (with index frame_index) uses candidate_reference as a
// reference frame.
bool CheckFrameUseReference(
    int frame_index, const GopFrame &frame, const GopFrame &candidate_reference,
    const std::vector<RefFrameTable> &ref_frame_table_list) {
  if (frame.update_type == GopFrameType::kOverlay ||
      frame.update_type == GopFrameType::kIntermediateOverlay) {
    return false;
  }
  for (const auto &ref_frame : frame.ref_frame_list) {
    if (ref_frame_table_list[frame_index][ref_frame.index].global_coding_idx ==
        candidate_reference.global_coding_idx) {
      return true;
    }
  }
  return false;
}

// Returns the number of frames that use this_frame as reference in the
// current and next subGop.
int CountUsedAsReference(const GopStruct &gop_struct,
                         const std::vector<LookaheadStats> &lookahead_stats,
                         const std::vector<RefFrameTable> &ref_frame_table_list,
                         const GopFrame &this_frame) {
  int num = 0;
  const int frame_count = static_cast<int>(gop_struct.gop_frame_list.size());
  // Check frames in this gop
  for (int i = 0; i < frame_count; ++i) {
    if (CheckFrameUseReference(i, gop_struct.gop_frame_list[i], this_frame,
                               ref_frame_table_list)) {
      ++num;
    }
  }
  // Check frames in the next gop
  if (!lookahead_stats.empty()) {
    const auto &next_gop_frame_list =
        lookahead_stats[0].gop_struct[0].gop_frame_list;
    const int next_gop_frame_count =
        static_cast<int>(next_gop_frame_list.size());
    for (int i = 0; i < next_gop_frame_count; ++i) {
      if (CheckFrameUseReference(frame_count + i, next_gop_frame_list[i],
                                 this_frame, ref_frame_table_list)) {
        ++num;
      }
    }
  }
  return num;
}

int GetIntArfQ(const GopStruct &gop_struct,
               const std::vector<LookaheadStats> &lookahead_stats,
               const std::vector<RefFrameTable> &ref_frame_table_list,
               const GopFrame &arf_frame, const GopFrame &int_arf_frame,
               int active_best_quality, int active_worst_quality) {
  if (!arf_frame.is_valid) return active_best_quality;
  assert(int_arf_frame.is_valid);

  // Check whether this is the first intermediate arf
  bool is_first_int_arf = false;
  for (const auto &gop_frame : gop_struct.gop_frame_list) {
    if (gop_frame.update_type == GopFrameType::kIntermediateArf) {
      is_first_int_arf =
          gop_frame.global_coding_idx == int_arf_frame.global_coding_idx;
      break;
    }
  }
  if (is_first_int_arf) {
    // int_arf_frame is the first intermediate arf in the subGop
    int arf_as_ref = CountUsedAsReference(gop_struct, lookahead_stats,
                                          ref_frame_table_list, arf_frame);
    int int_arf_as_ref = CountUsedAsReference(
        gop_struct, lookahead_stats, ref_frame_table_list, int_arf_frame);
    int arf_adjusted =
        arf_as_ref + static_cast<int>(int_arf_as_ref * kIntArfAdjFactor);
    if (arf_adjusted <= int_arf_as_ref) {
      return active_best_quality;
    } else {
      assert(arf_adjusted > 0);
      return active_best_quality +
             (active_worst_quality - active_best_quality) *
                 (arf_adjusted - int_arf_as_ref) / arf_adjusted;
    }
  } else {
    // int_arf_frame is not the first intermediate arf in the subGop
    assert(int_arf_frame.layer_depth >= 1);
    const int depth_factor = 1 << (int_arf_frame.layer_depth - 1);
    return (active_worst_quality * (depth_factor - 1) + active_best_quality) /
           depth_factor;
  }
}

StatusOr<GopEncodeInfo> AV1RateControlQMode::GetGopEncodeInfoWithNoStats(
    const GopStruct &gop_struct) {
  GopEncodeInfo gop_encode_info;
  const int frame_count = static_cast<int>(gop_struct.gop_frame_list.size());
  const int base_offset = av1_get_deltaq_offset(
      AOM_BITS_8, rc_param_.base_q_index, gop_struct.base_q_ratio);
  const int base_q_index = rc_param_.base_q_index + base_offset;
  for (int i = 0; i < frame_count; ++i) {
    FrameEncodeParameters param;
    const GopFrame &gop_frame = gop_struct.gop_frame_list[i];
    // Use constant QP for TPL pass encoding. Keep the functionality
    // that allows QP changes across sub-gop.
    param.q_index = base_q_index;
    param.rdmult = av1_compute_rd_mult_based_on_qindex(AOM_BITS_8, LF_UPDATE,
                                                       base_q_index);
    if (rc_param_.tpl_pass_count == TplPassCount::kTwoTplPasses) {
      if (gop_frame.update_type == GopFrameType::kRegularGolden ||
          gop_frame.update_type == GopFrameType::kRegularKey ||
          gop_frame.update_type == GopFrameType::kRegularArf) {
        if (rc_param_.tpl_pass_index) {
          param.q_index = kSecondTplPassQp;
          param.rdmult = av1_compute_rd_mult_based_on_qindex(
              AOM_BITS_8, ARF_UPDATE, kSecondTplPassQp);
        }
      }
    }
    gop_encode_info.param_list.push_back(param);
  }
  return gop_encode_info;
}

bool CheckFlash(const std::vector<FIRSTPASS_STATS> &stats_list, int index) {
  assert(index >= 1);
  return stats_list[index].is_flash || stats_list[index - 1].is_flash;
}

void inline SetUpFrameIndices(GopFrameType update_type, int stats_size,
                              int this_gop_len, int next_gop_len,
                              int &this_index, int &first_index,
                              int &last_index, int &ref_before_index,
                              int &ref_after_index) {
  if (update_type == GopFrameType::kRegularKey) {
    this_index = 0;
    first_index = 1;
    last_index = stats_size - 1;
    ref_before_index = -1;
    ref_after_index = -1;
  } else if (update_type == GopFrameType::kRegularGolden) {
    // TODO(b/260859962): Need to consider the situation when arf is not
    // used
    this_index = 0;
    first_index = 1;
    last_index = this_gop_len - 2;
    ref_before_index = -1;
    ref_after_index = this_gop_len - 1;
  } else {
    // arf type
    // TODO(b/260859962): It looks like in this case the last arf should
    // actually be at index -1. This for now should be accurate enough, but
    // in the future it is better to have the exact index of last arf.
    this_index = this_gop_len - 1;
    first_index = 1;
    last_index = next_gop_len >= 4 ? this_gop_len + next_gop_len - 2
                                   : this_gop_len + next_gop_len - 1;
    ref_before_index = 0;
    ref_after_index = next_gop_len >= 4 ? this_gop_len + next_gop_len - 1 : -1;
  }
}

// Return the accumulated score of a frame, considering its influence on the
// frames from first_index to last_index (both inclusive). When ref_before_index
// >= 0, only consider the frames where the current frame has a larger
// correlation than the frame at ref_before_index. Same for ref_after_index.
// This function also calculates and returns the average correlation coefficient
// of this frame to the affected frames through the parameter avg_correlation.
double GetAccumulatedScore(const FirstpassInfo &firstpass_info, int this_index,
                           int first_index, int last_index,
                           int ref_before_index, int ref_after_index,
                           double &avg_correlation) {
  assert(ref_before_index < 0 || ref_before_index < first_index);
  assert(ref_after_index < 0 || ref_after_index > last_index);
  double score = 0.0;
  int count = 0;
  avg_correlation = 0.0;
  // Check the influence of this frame to the frames before it
  for (int f = this_index - 1; f >= first_index; --f) {
    // The contribution of this frame to frame f
    double coeff_this = 1.0;
    for (int k = this_index; k > f; --k) {
      if (CheckFlash(firstpass_info.stats_list, k)) continue;
      coeff_this *= firstpass_info.stats_list[k].cor_coeff;
    }
    // The contribution of frame at ref_before_index to frame f
    if (ref_before_index >= 0) {
      double coeff_last = 1.0;
      for (int k = ref_before_index + 1; k <= f; ++k) {
        if (CheckFlash(firstpass_info.stats_list, k)) continue;
        coeff_last *= firstpass_info.stats_list[k].cor_coeff;
      }
      if (coeff_last > coeff_this) break;
    }
    ++count;
    avg_correlation += firstpass_info.stats_list[f + 1].cor_coeff;

    // If this is a flash, although we ignore it in the accumulation, we
    // still count it for this frame so it will probably have a low
    // correlation
    if (firstpass_info.stats_list[f].is_flash)
      coeff_this *= firstpass_info.stats_list[f].cor_coeff;

    const double this_cor =
        coeff_this * sqrt(std::max((firstpass_info.stats_list[f].intra_error -
                                    firstpass_info.stats_list[f].noise_var) /
                                       firstpass_info.stats_list[f].intra_error,
                                   0.5));
    score += this_cor;
  }

  // Check the influence of this frame to the frames after it
  for (int f = this_index + 1; f <= last_index; ++f) {
    // The contribution of this frame to frame f
    double coeff_this = 1.0;
    for (int k = this_index + 1; k <= f; ++k) {
      if (CheckFlash(firstpass_info.stats_list, k)) continue;
      coeff_this *= firstpass_info.stats_list[k].cor_coeff;
    }

    // The contribution of frame at ref_after_index to frame f
    if (ref_after_index >= 0) {
      double coeff_next = 1.0;
      for (int k = ref_after_index; k > f; --k) {
        if (CheckFlash(firstpass_info.stats_list, k)) continue;
        coeff_next *= firstpass_info.stats_list[k].cor_coeff;
      }
      if (coeff_next > coeff_this) break;
    }
    ++count;
    avg_correlation += firstpass_info.stats_list[f].cor_coeff;

    // If this is a flash, although we ignore it in the accumulation, we
    // still count it for this frame so it will probably have a low
    // correlation
    if (firstpass_info.stats_list[f].is_flash)
      coeff_this *= firstpass_info.stats_list[f].cor_coeff;

    const double this_cor =
        coeff_this * sqrt(std::max((firstpass_info.stats_list[f].intra_error -
                                    firstpass_info.stats_list[f].noise_var) /
                                       firstpass_info.stats_list[f].intra_error,
                                   0.5));
    score += this_cor;
  }
  if (count > 0) avg_correlation /= static_cast<double>(count);
  return score;
}

StatusOr<GopEncodeInfo> AV1RateControlQMode::GetGopEncodeInfoWithFp(
    const GopStruct &gop_struct, const FirstpassInfo &firstpass_info,
    const std::vector<LookaheadStats> &lookahead_stats,
    const RefFrameTable &ref_frame_table_snapshot_init) {
  const int frame_count = static_cast<int>(gop_struct.gop_frame_list.size());
  GopEncodeInfo gop_encode_info;

  const std::vector<RefFrameTable> ref_frame_table_list = GetRefFrameTableList(
      gop_struct, lookahead_stats, ref_frame_table_snapshot_init);
  gop_encode_info.final_snapshot = ref_frame_table_list[frame_count];

  const int stats_size = static_cast<int>(firstpass_info.stats_list.size());
  const FirstpassInfo analyzed_fp_info =
      AnalyzeFpStats(std::move(firstpass_info));

  const int this_gop_len = gop_struct.show_frame_count;
  const int next_gop_len =
      lookahead_stats.empty()
          ? 0
          : lookahead_stats[0].gop_struct[0].show_frame_count;
  if (stats_size < this_gop_len + next_gop_len) {
    Status status;
    status.code = AOM_CODEC_INVALID_PARAM;
    status.message = "The firstpass info length is insufficient.";
    return status;
  }

  GopFrame arf_frame = GopFrameInvalid();

  const int base_offset = av1_get_deltaq_offset(
      AOM_BITS_8, rc_param_.base_q_index, gop_struct.base_q_ratio);
  const int base_q_index = rc_param_.base_q_index + base_offset;
  const int active_worst_quality = base_q_index;
  int active_best_quality = base_q_index;
  for (int i = 0; i < frame_count; ++i) {
    FrameEncodeParameters param;
    const GopFrame &gop_frame = gop_struct.gop_frame_list[i];
    if (gop_frame.update_type == GopFrameType::kOverlay ||
        gop_frame.update_type == GopFrameType::kIntermediateOverlay ||
        gop_frame.update_type == GopFrameType::kRegularLeaf) {
      param.q_index = base_q_index;
    } else if (gop_frame.update_type == GopFrameType::kRegularKey ||
               gop_frame.update_type == GopFrameType::kRegularGolden ||
               gop_frame.update_type == GopFrameType::kRegularArf) {
      int this_index, first_index, last_index, ref_before_index,
          ref_after_index;
      SetUpFrameIndices(gop_frame.update_type, stats_size, this_gop_len,
                        next_gop_len, this_index, first_index, last_index,
                        ref_before_index, ref_after_index);

      double avg_correlation = 0;
      const double score = GetAccumulatedScore(
          analyzed_fp_info, this_index, first_index, last_index,
          ref_before_index, ref_after_index, avg_correlation);
      const double boost = std::min(
          std::max(sqrt(score), 1.0),
          gop_frame.update_type == GopFrameType::kRegularKey ? 6.0 : 4.0);
      const double qstep_ratio = 1.0 / boost;
      param.q_index = av1_get_q_index_from_qstep_ratio(base_q_index,
                                                       qstep_ratio, AOM_BITS_8);

      if (base_q_index) param.q_index = std::max(param.q_index, 1);
      active_best_quality = param.q_index;

      if (gop_frame.update_type == GopFrameType::kRegularArf) {
        arf_frame = gop_frame;
      }
    } else {
      param.q_index = GetIntArfQ(gop_struct, lookahead_stats,
                                 ref_frame_table_list, arf_frame, gop_frame,
                                 active_best_quality, active_worst_quality);
    }
    param.rdmult = GetRDMult(gop_frame, param.q_index);
    gop_encode_info.param_list.push_back(param);
  }
  return gop_encode_info;
}

StatusOr<GopEncodeInfo> AV1RateControlQMode::GetGopEncodeInfoWithTpl(
    const GopStruct &gop_struct, const FirstpassInfo &firstpass_info,
    const TplGopStats &tpl_gop_stats,
    const std::vector<LookaheadStats> &lookahead_stats,
    const RefFrameTable &ref_frame_table_snapshot_init) {
  assert(tpl_gop_stats.frame_stats_list.size() ==
         gop_struct.gop_frame_list.size());
  const int frame_count =
      static_cast<int>(tpl_gop_stats.frame_stats_list.size());

  GopEncodeInfo gop_encode_info;

  const std::vector<RefFrameTable> ref_frame_table_list = GetRefFrameTableList(
      gop_struct, lookahead_stats, ref_frame_table_snapshot_init);
  gop_encode_info.final_snapshot = ref_frame_table_list[frame_count];

  StatusOr<TplGopDepStats> gop_dep_stats = ComputeTplGopDepStats(
      gop_struct, tpl_gop_stats, lookahead_stats, ref_frame_table_list);
  if (!gop_dep_stats.ok()) {
    return gop_dep_stats.status();
  }

  const int stats_size = static_cast<int>(firstpass_info.stats_list.size());

  const int this_gop_len = gop_struct.show_frame_count;
  const int next_gop_len =
      lookahead_stats.empty()
          ? 0
          : lookahead_stats[0].gop_struct[0].show_frame_count;
  if (stats_size < this_gop_len + next_gop_len) {
    Status status;
    status.code = AOM_CODEC_INVALID_PARAM;
    status.message = "The firstpass info length is insufficient.";
    return status;
  }

  const int base_offset = av1_get_deltaq_offset(
      AOM_BITS_8, rc_param_.base_q_index, gop_struct.base_q_ratio);
  const int base_q_index = rc_param_.base_q_index + base_offset;

  const int active_worst_quality = base_q_index;
  int active_best_quality = base_q_index;

  for (int i = 0; i < frame_count; ++i) {
    FrameEncodeParameters param;
    const GopFrame &gop_frame = gop_struct.gop_frame_list[i];

    if (gop_frame.update_type == GopFrameType::kOverlay ||
        gop_frame.update_type == GopFrameType::kIntermediateOverlay ||
        gop_frame.update_type == GopFrameType::kRegularLeaf) {
      param.q_index = base_q_index;
    } else if (gop_frame.update_type == GopFrameType::kRegularGolden ||
               gop_frame.update_type == GopFrameType::kRegularKey ||
               gop_frame.update_type == GopFrameType::kRegularArf) {
      const TplFrameDepStats &frame_dep_stats =
          gop_dep_stats->frame_dep_stats_list[i];

      double frame_importance;
      // TODO(jingning): Temporarily make the switch between single and
      // two TPL passes depending on the availability. This part of code
      // needs further modifications to support SB level calculation.
      if (rc_param_.tpl_pass_count == TplPassCount::kTwoTplPasses) {
        frame_importance = GetFrameImportanceTwoPassTpl(
            frame_dep_stats, rc_param_.frame_width, rc_param_.frame_height);
      } else {
        const double cost_without_propagation =
            TplFrameDepStatsAccumulateIntraCost(frame_dep_stats);
        const double cost_with_propagation =
            TplFrameDepStatsAccumulate(frame_dep_stats);
        frame_importance = cost_with_propagation / cost_without_propagation;
      }

      // Imitate the behavior of av1_tpl_get_qstep_ratio()
      const double qstep_ratio = sqrt(1 / frame_importance);
      param.q_index = av1_get_q_index_from_qstep_ratio(base_q_index,
                                                       qstep_ratio, AOM_BITS_8);

      if (base_q_index) param.q_index = AOMMAX(param.q_index, 1);
      active_best_quality = param.q_index;

      if (rc_param_.max_distinct_q_indices_per_frame > 1) {
        std::vector<uint8_t> superblock_q_indices;
        superblock_q_indices = SetupDeltaQ(
            frame_dep_stats, rc_param_.frame_width, rc_param_.frame_height,
            param.q_index, frame_importance,
            rc_param_.tpl_pass_count == TplPassCount::kTwoTplPasses);
        std::unordered_map<int, int> qindex_centroids = internal::KMeans(
            superblock_q_indices, rc_param_.max_distinct_q_indices_per_frame);
        for (size_t i = 0; i < superblock_q_indices.size(); ++i) {
          const int curr_sb_qindex =
              qindex_centroids.find(superblock_q_indices[i])->second;
          const int delta_q_res = 4;
          const int adjusted_qindex =
              param.q_index +
              (curr_sb_qindex - param.q_index) / delta_q_res * delta_q_res;
          const int rd_mult = GetRDMult(gop_frame, adjusted_qindex);
          param.superblock_encode_params.push_back(
              { static_cast<uint8_t>(adjusted_qindex), rd_mult });
        }
      }
    } else {
      // TODO(b/259601830): Also consider using GetIntArfQ here.
      // Intermediate ARFs
      assert(gop_frame.layer_depth >= 1);
      const int depth_factor = 1 << (gop_frame.layer_depth - 1);
      param.q_index =
          (active_worst_quality * (depth_factor - 1) + active_best_quality) /
          depth_factor;
    }
    param.rdmult = GetRDMult(gop_frame, param.q_index);
    gop_encode_info.param_list.push_back(param);
  }
  return gop_encode_info;
}

StatusOr<GopEncodeInfo> AV1RateControlQMode::GetTplPassGopEncodeInfo(
    const GopStruct &gop_struct,
    const FirstpassInfo &firstpass_info AOM_UNUSED) {
  return GetGopEncodeInfoWithNoStats(gop_struct);
}

StatusOr<GopEncodeInfo> AV1RateControlQMode::GetGopEncodeInfo(
    const GopStruct &gop_struct, const TplGopStats &tpl_gop_stats,
    const std::vector<LookaheadStats> &lookahead_stats,
    const FirstpassInfo &firstpass_info AOM_UNUSED,
    const RefFrameTable &ref_frame_table_snapshot_init) {
  // When TPL stats are not valid, use first pass stats.
  Status status = ValidateTplStats(gop_struct, tpl_gop_stats);
  if (!status.ok()) {
    return status;
  }

  for (const auto &lookahead_stat : lookahead_stats) {
    Status status = ValidateTplStats(*lookahead_stat.gop_struct,
                                     *lookahead_stat.tpl_gop_stats);
    if (!status.ok()) {
      return status;
    }
  }

  return GetGopEncodeInfoWithTpl(gop_struct, firstpass_info, tpl_gop_stats,
                                 lookahead_stats,
                                 ref_frame_table_snapshot_init);
}

}  // namespace aom
